#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import re
import csv
import pexpect
from io import StringIO
import functools
from typing import Union

from common.logger import Logger
from common.err_code import CommErrCode
from saphanay.comm.common_utils import run_subprocess
from saphanay.comm.error_code_exception import ErrorCodeException
from saphanay.comm.constants import SAPHANAErrorCode

logger = Logger().get_logger()


class SAPHANA:

    """SAP HANA 管理"""

    def __init__(self, sid: str, db_name: str, port: str, db_user: str, db_pwd: str) -> None:
        """
        :param sid: 系统 id ，例如 S00
        :param db_name: 数据库名，例如 DATA1
        :param port: 系统数据库端口，例如 30067
        :param db_user: 数据库用户，例如 SYSTEM
        :param db_pwd: 数据库密码，例如 xxx
        """
        self._sid = sid
        self._db_name = db_name
        self._port = port
        self._db_user = db_user
        self._db_pwd = db_pwd

        # 运行时状态
        self.__hdbsql_process = None

    @functools.cache
    def get_instance_number(self) -> str:
        """
        :raise ErrorCodeException:
        """
        command = f"su - {self._sid.lower()}adm -c 'echo $TINSTANCE'"
        tinstance = run_subprocess(command).stdout.strip()
        if not tinstance:
            logger.error("Environment variable TINSTANCE missing.")
            raise ErrorCodeException(SAPHANAErrorCode.ENVIRONMENTS_NOT_SET, "TINSTANCE")
        if not tinstance.isdigit():
            logger.error(f"Environment variable TINSTANCE is not digit. {[tinstance]}")
            raise ErrorCodeException(SAPHANAErrorCode.ENVIRONMENTS_NOT_SET, "TINSTANCE")
        return tinstance

    @functools.cache
    def get_system_hostnames(self) -> list[str]:
        """
        :raise ErrorCodeException:
        """
        command = (f"su - {self._sid.lower()}adm -c '"
                   f"sapcontrol -nr {self.get_instance_number()} -format script -function GetSystemInstanceList'")
        process = run_subprocess(command)
        hostnames = re.findall(r"\d+ hostname: (\S+)", process.stdout)
        if not hostnames:
            logger.error(f"Failed to get hostnames. {process.stdout}")
            raise ErrorCodeException(CommErrCode.OPERATION_FAILED)
        return hostnames

    def login_database(self) -> None:
        """
        登录数据库，保存登录状态 self.__hdbsql_process 。
        :raise ErrorCodeException:
        """
        address = ",".join([f"{h}:{self._port}" for h in self.get_system_hostnames()])
        command = (f"su - {self._sid.lower()}adm -c '"
                   f"hdbsql -i {self.get_instance_number()} -n {address} -quiet -resultencoding UTF8 -x -j -a "
                   f"-d {self._db_name.upper()} -u {self._db_user.upper()}'")
        logger.info(f"Login database: {command}")

        try:
            self.__hdbsql_process = pexpect.spawn(command, encoding="utf_8", timeout=30)
            self.__hdbsql_process.expect("Password:", timeout=30)
            self.__hdbsql_process.sendline(self._db_pwd)
            self.__hdbsql_process.expect(r"hdbsql\s+\S+=>\s+", timeout=30)
        except pexpect.TIMEOUT as e:
            logger.error(f"Timeout while logging database. {self._sid} {self._db_name} {e}", exc_info=1)
            raise ErrorCodeException(CommErrCode.FAILED_EXECUTE_COMMAND, command, e)
        except Exception as e:
            logger.error(f"Exception occurred while logging database. {self._sid} {self._db_name} {e}", exc_info=1)
            raise ErrorCodeException(CommErrCode.FAILED_EXECUTE_COMMAND, command, e)
        logger.info(f"Login database successfully. {self._sid} {self._db_name}")

    def execute_sql_command(self, sql: str, is_query: bool = True) -> list[list[str]]:
        """
        执行 SQL 语句。若是查询，返回二维列表。
        :return: 例如 [["1739419031619", "topology", "hana1"], ["1739427439813", "topology", "hana1"]]
        :raise ErrorCodeException:
        """
        not self.__hdbsql_process and self.login_database()

        logger.info(f"Execute sql command. {self._sid} {self._db_name} {sql}")

        try:
            self.__hdbsql_process.sendline(sql)
            # 非查询 不设置超时
            self.__hdbsql_process.expect(r"hdbsql\s+\S+=>\s+", timeout=30 if is_query else -1)
            before = self.__hdbsql_process.before
        except pexpect.TIMEOUT as e:
            logger.error(f"Timeout while executing sql command. {sql} {e}", exc_info=1)
            raise ErrorCodeException(CommErrCode.FAILED_EXECUTE_COMMAND, sql, e)
        except Exception as e:
            logger.error(f"Exception occurred while executing sql command. {sql} {e}", exc_info=1)
            raise ErrorCodeException(CommErrCode.FAILED_EXECUTE_COMMAND, sql, e)

        if not is_query:
            return []

        try:
            data = list(csv.reader(StringIO(before)))
        except Exception as e:
            logger.error(f"Failed to parse SQL result data. {before} {e}", exc_info=1)
            raise ErrorCodeException(CommErrCode.FAILED_EXECUTE_COMMAND, sql, before)

        # 移除首行 SQL 语句回显
        if data[0][0].upper().startswith("SELECT"):
            data = data[1:]

        if not data:
            logger.warning(f"Got no data. {self._sid} {self._db_name} {sql}")
            return []

        logger.debug(f"Got {len(data)} rows of data. {self._sid} {self._db_name} {sql} {data[:100]}")
        return data

    def configure_backint(self, enable: bool) -> None:
        """
        启用或禁用 backint 。仅系统数据库有效。
        :raise ErrorCodeException:
        """
        if not self._db_name == "SYSTEMDB":
            logger.warning(f"Tenant database cannot configure backint {enable}. {self._sid} {self._db_name}")
            return
        if enable:
            sql = """
alter system alter configuration ('global.ini', 'SYSTEM') set ('backup', 'log_backup_using_backint') = 'true' with reconfigure;
alter system alter configuration ('global.ini', 'SYSTEM') set ('backup', 'catalog_backup_using_backint') = 'true' with reconfigure;
"""
        else:
            sql = """
alter system alter configuration ('global.ini', 'SYSTEM') unset ('backup', 'log_backup_using_backint') with reconfigure;
alter system alter configuration ('global.ini', 'SYSTEM') unset ('backup', 'catalog_backup_using_backint') with reconfigure;
"""
        logger.info(f"Configure backint {enable}.")
        _ = self.execute_sql_command(sql, is_query=False)
